package pascalcompiler.core;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Stack;

/**
 *
 * @author Eduardo Freire
 */
public class Complicador {
    public static final int INTEGER = 0;
    public static final int REAL = 1;
    public static final int BOOLEAN = 2;
    public static final int ARRAY_INTEGER = 3;
    public static final int ARRAY_REAL = 4;
    public static final int ARRAY_BOOLEAN = 5;
    public static final int NONE = 6;

    public static final int CONST = 0;
    public static final int VAR = 1;
    public static final int PARAM = 2;
    public static final int PROC = 3;
    public static final int FUNC = 4;

    private static Complicador instance;
    private Stack<Escopo> pilha = new Stack<Escopo>();
    private String ID;
    private Escopo escopo;
    private Token token;
    private int lineCount;
    private Scanner in;
    private char currentChar;
    private HashSet<String> palavrasReservadas = new HashSet(Arrays.asList(new String[] {
        "PROGRAM", "VAR", "CONST", "BEGIN", "END", "IF", "THEN", "ELSE", "FOR", "WHILE", "TO",
        "DOWNTO", "DO", "OF", "INTEGER", "REAL", "BOOLEAN", "TRUE", "FALSE", "FUNCTION", "PROCEDURE",
        "+", "-", "*", "/", "=", ":=", "<", ">", "<=", ">=", "<>", "." ,"..", ",",
        ":", ";", "{", "}","[", "]", "(", ")", "NOT", "DIV", "MOD", "AND", "OR"
    }));

    private String[] tipos = {
        "INTEGER", "REAL", "BOOLEAN",
        "ARRAY OF INTEGER", "ARRAY OF REAL", "ARRAY OF BOOLEAN", "NONE"
    };

    private boolean isReservada() {
        return palavrasReservadas.contains(token.getString());
    }

    public static Complicador getInstance() {
        if(instance == null)
            instance = new Complicador();
        return instance;
    }

    public void checkCode(String inputCode) throws Exception {
        this.lineCount = 1;
        this.in = new Scanner(inputCode).useDelimiter("");
        this.token = new Token();
        this.pilha.clear();

        try {
            nextChar();
            nextToken();
            checkPrograma();
        }
        catch(NullPointerException ex) {
            ex.printStackTrace();
        }

        System.out.println("\n---x---x---x---x---x---x---\n");
    }

    public int getLineCount() {
        return lineCount;
    }

    private void nextToken() throws Exception {
        token.clear();

        if(!skipWhite()) {
            return;
        }

        if(Character.isLetter(currentChar)) {
            getName();
        }
        else if(Character.isDigit(currentChar)) {
            getNum();
        }
        else {
            getOp();
        }

        System.out.println(" |" + token.getString() + "|");
    }

    private boolean skipWhite() {
        while(currentChar == ' ' || currentChar == '\n' || currentChar == '\t') {
            if(currentChar == '\n') {
                lineCount++;
            }
            if(!nextChar()) {
                break;
            }
        }
        if(currentChar == '{') {
            do {
                System.out.print(currentChar);
                if(currentChar == '\n') {
                    lineCount++;
                }
                if(!nextChar()) {
                    return false;
                }
            } while(currentChar != '}');
            System.out.println(currentChar);
            
            if(!nextChar() || !skipWhite()) {
                return false;
            }
        }

        return true;
    }

    private void getName() {
        while(Character.isLetterOrDigit(currentChar) || currentChar == '_') {
            token.add(currentChar);

            if(!nextChar()) {
                break;
            }
        }
        token.setID(true);
    }

    private void getNum() throws Exception {
        while(Character.isDigit(currentChar)) {
            token.add(currentChar);

            if(!nextChar()) {
                token.setID(false);
                token.setTipo(INTEGER);
                return;
            }
        }

        if(currentChar == '.') {
            token.add(currentChar);

            if(!nextChar()) {
                token.setID(false);
                token.setTipo(REAL);
                return;
            }
        }
        else {
            token.setID(false);
            token.setTipo(INTEGER);
            return;
        }

        while(Character.isDigit(currentChar)) {
            token.add(currentChar);

            if(!nextChar()) {
                token.setID(false);
                token.setTipo(REAL);
                return;
            }
        }
        
        if(currentChar == 'e' || currentChar == 'E') {
            token.add(currentChar);

            if(!nextChar()) {
                throw new Exception("Token inválido: " + token.getString());
            }
        }
        else {
            token.setID(false);
            token.setTipo(REAL);
            return;
        }

        if(currentChar == '+' || currentChar == '-') {
            token.add(currentChar);

            if(!nextChar()) {
                throw new Exception("Token inválido: " + token.getString());
            }
        }

        if(!Character.isDigit(currentChar)) {
            throw new Exception("Token inválido: " + token.getString());
        }

        while(Character.isDigit(currentChar)) {
            token.add(currentChar);

            if(!nextChar()) {
                token.setID(false);
                token.setTipo(REAL);
                return;
            }
        }

        token.setID(false);
        token.setTipo(REAL);
        return;
    }

    private void getOp() throws Exception {
        if(currentChar == '.' || currentChar == ':' ||
              currentChar == '<' || currentChar == '>') {
            while(currentChar == '=' || currentChar == ':' || currentChar == '.' ||
                  currentChar == '<' || currentChar == '>') {
                token.add(currentChar);

                if(!nextChar()) {
                    break;
                }
            }
        }
        else if(currentChar == '+' || currentChar == '-' || currentChar == ';' ||
            currentChar == '*' || currentChar == '/' || currentChar == '=' ||
            currentChar == '(' || currentChar == ')' || currentChar == ',' ||
            currentChar == '[' || currentChar == ']') {
            token.add(currentChar);

            nextChar();
        }

        if(!token.isEmpty() && !isReservada()) {
            throw new Exception("Token inválido: " + token.getString());
        }

        token.setID(false);
        token.setTipo(NONE);
    }

    private boolean nextChar() {
        if(in.hasNext()) {
            currentChar = in.next().charAt(0);
            return true;
        }

        currentChar = '\0';
        return false;
    }

    public void checkPrograma() throws Exception {
        abrirEscopo();

        check("PROGRAM");
        checkIdentificador();
        check(";");
        escopo.flush(NONE, NONE);

        checkDeclaracoes();
        checkDeclaracoesDeSubprogramas();

        escopo.print();

        checkComandoComposto();
        checkFinalDePrograma();
    }

    private void checkFinalDePrograma() throws Exception {
        check(".");
        if(!token.isEmpty()) {
            throw new Exception("Token inesperado: " + token.getString());
        }
    }

    private boolean isIdentificador(boolean add) throws Exception {
        if(token.isID() && !isReservada()) {
            ID = token.getString();
            if(add) {
                escopo.add(token.getString());
            }
            nextToken();
            return true;
        }
        return false;
    }

    private void checkIdentificador() throws Exception {
        if(!token.isID()) {
            throw new Exception("Esperado identificador");
        }
        if(isReservada()) {
            throw new Exception("Palavra reservada não pode ser identificador: " + token.getString());
        }
        
        escopo.add(token.getString());
        ID = token.getString();
        nextToken();
    }

    private void checkDeclaracoes() throws Exception {
        checkDeclaracaoConstante();
        checkDeclaracaoVariavel();
    }

    private void checkDeclaracaoConstante() throws Exception {
        checkDeclaracaoConstante2();
    }

    private void checkDeclaracaoConstante2() throws Exception {
        if(is("CONST")) {
            checkIdentificador();
            check("=");
            checkConstante();
            check(";");
            checkDeclaracaoConstante2();
        }
    }

    private void checkDeclaracaoVariavel() throws Exception {
        if(is("VAR")) {
            checkListaDeclaracaoVariavel();
        }
    }

    private void checkListaDeclaracaoVariavel() throws Exception {
        checkListaDeIdentificadores();
        check(":");
        checkTipo(VAR);
        check(";");
        checkListaDeclaracaoVariavel2();
    }

    private void checkListaDeclaracaoVariavel2() throws Exception {
        if(isListaDeIdentificadores()) {
            check(":");
            checkTipo(VAR);
            check(";");
            checkListaDeclaracaoVariavel2();
        }
    }

    private boolean isListaDeIdentificadores() throws Exception {
        if(isIdentificador(true)) {
            checkListaIdentificadores2();
            return true;
        }
        return false;
    }

    private void checkListaDeIdentificadores() throws Exception {
        checkIdentificador();
        checkListaIdentificadores2();
    }

    private void checkListaIdentificadores2() throws Exception {
        if(is(",")) {
            checkIdentificador();
            checkListaIdentificadores2();
        }
    }

    private void checkTipo(int natureza) throws Exception {
        if(!isTipoPadrao(natureza, false) && !isTipoArray(natureza)) {
            throw new Exception("Esperado tipo padrão ou array para variavel");
        }

    }

    private void checkConstante() throws Exception {
        if(token.getTipo() == INTEGER) {
            escopo.flush(CONST, INTEGER);
            nextToken();
        }
        else if(token.getTipo() == REAL) {
            escopo.flush(CONST, REAL);
            nextToken();
        }
        else if(token.is("TRUE") || token.is("FALSE")) {
            escopo.flush(CONST, BOOLEAN);
            nextToken();
        }
        else {
            throw new Exception("Esperado valor inteiro, real ou booleano para constante");
        }
    }

    private boolean isInt() throws Exception {
        if(token.getTipo() == INTEGER) {
            nextToken();
            return true;
        }
        return false;
    }

    private boolean isReal() throws Exception {
        if(token.getTipo() == REAL) {
            nextToken();
            return true;
        }
        return false;
    }

    private boolean isBoolean() throws Exception {
        if(!token.is("TRUE") && !token.is("FALSE")) {
            return false;
        }
        nextToken();
        return true;
    }

    private boolean isTipoPadrao(int natureza, boolean array) throws Exception {
        int tipo;

        if(token.is("INTEGER")) {
            tipo = array ?  ARRAY_INTEGER : INTEGER;

            escopo.flush(natureza, tipo);
            nextToken();
            return true;
        }
        if(token.is("REAL")) {
            tipo = array ? ARRAY_REAL : REAL;
            
            escopo.flush(natureza, tipo);
            nextToken();
            return true;
        }
        if(token.is("BOOLEAN")) {
            tipo = array ? ARRAY_BOOLEAN : BOOLEAN;
            
            escopo.flush(natureza, tipo);
            nextToken();
            return true;
        }

        return false;
    }

    private int checkTipoPadrao(int natureza, boolean array) throws Exception {
        int tipo;

        if(token.is("INTEGER")) {
            tipo = array ?  ARRAY_INTEGER : INTEGER;

            escopo.flush(natureza, tipo);
            nextToken();
            return tipo;
        }
        if(token.is("REAL")) {
            tipo = array ?  ARRAY_REAL : REAL;

            escopo.flush(natureza, tipo);
            nextToken();
            return tipo;
        }
        if(token.is("BOOLEAN")) {
            tipo = array ? ARRAY_BOOLEAN : BOOLEAN;

            escopo.flush(natureza, tipo);
            nextToken();
            return tipo;
        }

        throw new Exception("Esperado tipo padrão");
    }

    private boolean isTipoArray(int natureza) throws Exception {
        if(!is("ARRAY")) return false;
        check("[");
        if(!isInt()) throw new Exception("Esperado valor inteiro");
        check("..");
        if(!isInt()) throw new Exception("Esperado valor inteiro");
        check("]");
        check("OF");
        checkTipoPadrao(natureza, true);

        return true;
    }

    private void checkComandoComposto() throws Exception {
        check("BEGIN");
        checkComandosOpcionais();
        check("END");
    }

    private boolean isComandoComposto() throws Exception {
        if(is("BEGIN")) {
            checkComandosOpcionais();
            check("END");
            return true;
        }
        return false;
    }

    private void checkComandosOpcionais() throws Exception {
        checkListaDeComandos();
    }

    private void checkListaDeComandos() throws Exception {
        if(isComando()) {
            checkListaComandos2();
        }
    }

    private void checkListaComandos2() throws Exception {
        if(is(";")) {
            checkComando();
            checkListaComandos2();
        }
    }

    private void checkComando() throws Exception {
        if(!isAtribuicaoOuAtivacao() &&
           !isIf() &&
           !isComandoComposto() &&
           !isWhile() &&
           !isFor()) {
            throw new Exception("Esperado Comando");
        }
    }

    private boolean isComando() throws Exception {
        if(isAtribuicaoOuAtivacao() ||
            isIf() ||
            isComandoComposto() ||
            isWhile() ||
            isFor()) {
            return true;
        }
        return false;
    }

    private boolean isAtribuicaoOuAtivacao() throws Exception {
        if(isIdentificador(false)) {
            if(isAtribuicaoDeVariavel()) {
                return true;
            }
            else if(isAtivacaoDeProcedimento()) {
                return true;
            }
        }
        return false;
    }

    private boolean isAtribuicaoDeVariavel() throws Exception {
        Identificador id = checkDeclaracaoDeIdentificador();

        if(is("[")) {
            checkTipoInteger(checkExpressao());
            check("]");
        }
        if(is(":=")) {
            if(id.getNatureza() != PARAM && id.getNatureza() != VAR) {
                throw new Exception("Esperado variável");
            }

            checkTipos(id.getTipo(), checkExpressao(), false);
            return true;
        }
        return false;
    }

    private boolean isAtivacaoDeProcedimento() throws Exception {
        Identificador id = checkDeclaracaoDeIdentificador();

        if(id.getNatureza() != FUNC && id.getNatureza() != PROC) {
            throw new Exception("Esperado ativação de procedimento");
        }

        if(is("(")) {
            checkListaDeExpressoes(id.getParametros().iterator());
            check(")");
        }
        else if(!id.getParametros().isEmpty()) {
            throw new Exception("Número incompatível de argumentos");
        }
        return true;
    }

    private boolean isIf() throws Exception {
        if(is("IF")) {
            checkTipoBoolean(checkExpressao());
            check("THEN");
            checkComando();
            checkElse();
            return true;
        }
        return false;
    }

    private boolean isWhile() throws Exception {
        if(is("WHILE")) {
            checkTipoBoolean(checkExpressao());
            check("DO");
            checkComando();
            return true;
        }
        return false;
    }

    private boolean isFor() throws Exception {
        if(is("FOR")) {
            checkIdentificador();
            check(":=");
            checkTipoInteger(checkExpressao());
            check("TO", "DOWNTO");
            checkTipoInteger(checkExpressao());
            check("DO");
            checkComando();
            return true;
        }
        return false;
    }

    private void checkElse() throws Exception {
        if(is("ELSE")) {
            checkComando();
        }
    }

    private int checkExpressao() throws Exception {
        return checkRelacional(checkExpressaoSimples(), checkExpressaoRelacional());
    }

    private int checkExpressaoRelacional() throws Exception {
        if(is("=") || is("<") || is(">") || is("<=") || is(">=") || is("<>")) {
            return checkExpressaoSimples();
        }
        return -1;
    }

    private int checkExpressaoAditiva() throws Exception {
        if(is("+") || is("-")) {
            return checkAritmetico(checkTermo(), checkExpressaoAditiva());
        }
        if(is("OR")) {
            return checkLogica(checkTermo(), checkExpressaoAditiva());
        }
        return -1;
    }

    private int checkExpressaoMultiplicativa() throws Exception {
        if(is("*") || is("/") || is("DIV") || is("MOD")) {
            return checkAritmetico(checkFator(), checkExpressaoMultiplicativa());
        }
        else if(is("AND")) {
            return checkLogica(checkFator(), checkExpressaoMultiplicativa());
        }
        return -1;
    }
    
    private int checkExpressaoSimples() throws Exception {
        if((is("+") || is("-")) && isBoolean()) {
            throw new Exception("Esperado tipo INTEGER ou REAL após sinal");
        }
        return checkTipos(checkTermo(), checkExpressaoAditiva(), true);
    }

    private int checkTermo() throws Exception {
        return checkTipos(checkFator(), checkExpressaoMultiplicativa(), true);
    }

    private int checkFator() throws Exception {
        if(isIdentificador(false)) {
            Identificador id = checkDeclaracaoDeIdentificador();
            int tipo = id.getTipo();

            if(id.getNatureza() == PROC) {
                throw new Exception("PROCEDURE não retorna valor");
            }

            if(is("[")) {
                if(tipo != ARRAY_INTEGER && tipo != ARRAY_REAL && tipo != ARRAY_BOOLEAN) {
                    throw new Exception("Esperado ARRAY");
                }
                tipo -= 3;
                checkTipoInteger(checkExpressao());
                check("]");
            }
            else if(is("(")) {
                if(id.getNatureza() != FUNC) {
                    throw new Exception("Esperado função");
                }
                checkListaDeExpressoes(id.getParametros().iterator());
                check(")");
            }
            else if(id.getNatureza() == FUNC && !id.getParametros().isEmpty()) {
                throw new Exception("Número incompatível de argumentos");
            }
            //System.out.println("FATOR TIPO: " + tipo);
            return tipo;
        }
        else if(is("(")) {
            int tipo = checkExpressao();
            check(")");
            //System.out.println("FATOR TIPO: " + tipo);
            return tipo;
        }
        else if(is("NOT")) {
            return checkFator();
        }
        else if(isInt()) {
            //System.out.println("FATOR TIPO: " + INTEGER[CONST]);
            return INTEGER;
        }
        else if(isReal()) {
            //System.out.println("FATOR TIPO: " + REAL[CONST]);
            return REAL;
        }
        else if(isBoolean()) {
            //System.out.println("FATOR TIPO: " + BOOLEAN[CONST]);
            return BOOLEAN;
        }
        else {
            throw new Exception("Esperada expressão");
        }
    }

    private void checkListaDeExpressoes(Iterator<Integer> it) throws Exception {
        if(!it.hasNext()) {
            throw new Exception("Número incompatível de argumentos");
        }
        checkTipos(it.next().intValue(), checkExpressao(), false);
        checkListaExpressoes2(it);
    }

    private void checkListaExpressoes2(Iterator<Integer> it) throws Exception {
        if(is(",")) {
            if(!it.hasNext()) {
                throw new Exception("Número incompatível de argumentos");
            }
            checkTipos(it.next().intValue(), checkExpressao(), false);
            checkListaExpressoes2(it);
        }
        else if(it.hasNext()) {
            throw new Exception("Número incompatível de argumentos");
        }
    }

    private void checkDeclaracoesDeSubprogramas() throws Exception {
        checkDeclaracoesDeSubprogramas2();
    }

    private void checkDeclaracoesDeSubprogramas2() throws Exception {
        if(isCabecalhoDeSubprograma()) {
            checkDeclaracoesDeSubprogramas2();
        }
    }

    private boolean isCabecalhoDeSubprograma() throws Exception {
        if(is("FUNCTION")) {
            Identificador functionID = new Identificador(FUNC, NONE);
            String functionNome = token.getString();

            escopo.add(functionNome, functionID);
            abrirEscopo();

            checkIdentificador();
            escopo.add(functionNome, functionID);
            checkArgumentos();
            check(":");
            functionID.setTipo( checkTipoPadrao(FUNC, false) );
            check(";");

            checkDeclaracoes();
            checkDeclaracoesDeSubprogramas();
            checkComandoComposto();

            escopo.print();
            fecharEscopo();

            functionID.printParametros();

            return true;
        }
        else if(is("PROCEDURE")) {
            Identificador procedureID = new Identificador(PROC, NONE);
            String procedureNome = token.getString();

            escopo.add(procedureNome, procedureID);
            abrirEscopo();

            checkIdentificador();
            escopo.add(procedureNome, procedureID);
            checkArgumentos();
            check(";");

            checkDeclaracoes();
            checkDeclaracoesDeSubprogramas();
            checkComandoComposto();

            escopo.print();
            fecharEscopo();

            procedureID.printParametros();

            return true;
        }
        return false;
    }

    private void checkArgumentos() throws Exception {
        if(is("(")) {
            checkListaDeParametros();
            check(")");
        }
    }

    private void checkListaDeParametros() throws Exception {
        checkListaDeIdentificadores();
        check(":");
        checkTipo(PARAM);
        checkListaDeParametros2();
    }

    private void checkListaDeParametros2() throws Exception {
        if(is(";")) {
            checkListaDeIdentificadores();
            check(":");
            checkTipo(PARAM);
            checkListaDeParametros2();
        }
    }

    private boolean is(String s) throws Exception {
        if(!token.is(s)) {
            return false;
        }
        nextToken();
        return true;
    }

    private void check(String s) throws Exception {
        if(!token.is(s)) {
            throw new Exception("Esperado " + s + " encontrado " + token.getString());
        }
        nextToken();
    }

    private void check(String s1, String s2) throws Exception {
        if(!token.is(s1) && !token.is(s2)) {
            throw new Exception("Esperado " + s1 + " ou " + s2 + " encontrado " + token.getString());
        }
        nextToken();
    }

    private int checkTipos(int tipo1, int tipo2, boolean coercao) throws Exception {
        if(tipo2 == -1) {
            return tipo1;
        }
        if(tipo2 == PROC) {
            throw new Exception("PROCEDURE não retorna valor");
        }

        if(tipo1 == REAL && tipo2 == INTEGER) {
            return REAL;
        }
        if(coercao && tipo1 == INTEGER && tipo2 == REAL) {
           return REAL;
        }
        if(tipo1 == tipo2) {
            return tipo1;
        }

        throw new Exception("Tipos incompatíveis: " + tipos[tipo1] + " e " + tipos[tipo2]);
    }

    private void checkTipoInteger(int tipo) throws Exception {
        if(tipo != INTEGER) {
            throw new Exception("Esperado tipo INTEGER");
        }
    }

    private void checkTipoBoolean(int tipo) throws Exception {
        if(tipo != BOOLEAN) {
            throw new Exception("Esperado tipo BOOLEAN");
        }
    }

    private int checkRelacional(int tipo1, int tipo2) throws Exception {
        if(tipo2 == -1) {
            return tipo1;
        }

        if((tipo1 == INTEGER && tipo2 == INTEGER) ||
           (tipo1 == INTEGER && tipo2 == REAL) ||
           (tipo1 == REAL && tipo2 == INTEGER) ||
           (tipo1 == REAL && tipo2 == REAL)) {
            return BOOLEAN;
        }

        throw new Exception("Esperado tipo INTEGER ou REAL em operação relacional");
    }

    private int checkAritmetico(int tipo1, int tipo2) throws Exception {
        if(tipo2 == -1) {
            if(tipo1 != BOOLEAN) {
                return tipo1;
            }
            throw new Exception("Esperado tipo INTEGER ou REAL em operação aritmética");
        }

        if(tipo1 != INTEGER && tipo1 != REAL) {
            throw new Exception("Esperado tipo INTEGER ou REAL em operação aritmética");
        }
        if(tipo2 != INTEGER && tipo2 != REAL) {
            throw new Exception("Esperado tipo INTEGER ou REAL em operação aritmética");
        }
        if(tipo1 == REAL || tipo2 == REAL) {
            return REAL;
        }
        else {
            return INTEGER;
        }
    }

    private int checkLogica(int tipo1, int tipo2) throws Exception {
        if(tipo2 == -1) {
            if(tipo1 == BOOLEAN) {
                return BOOLEAN;
            }
            throw new Exception("Esperado tipo BOOLEAN em operação lógica");
        }

        if(tipo1 != BOOLEAN || tipo2 != BOOLEAN) {
            throw new Exception("Esperado tipo BOOLEAN em operação lógica");
        }

        return BOOLEAN;
    }

    private void abrirEscopo() {
        System.out.println("~~~~~~ABRIU~~~~~~");
        escopo = new Escopo();
        pilha.add(escopo);
    }

    private void fecharEscopo() throws Exception {
        System.out.println("~~~~~~FECHOU~~~~~~");
        pilha.pop();
        escopo = pilha.lastElement();
    }

    private Identificador checkDeclaracaoDeIdentificador() throws Exception {
        Iterator<Escopo> it = pilha.iterator();
        Identificador id;

        while(it.hasNext()) {
            id = it.next().contains(ID);

            if(id != null) {
                return id;
            }
        }

        throw new Exception("Identificador não declarado: " + ID);
    }
}